#include "RenderMethodDirect3D.h"

namespace YoghurtGum
{

	RenderMethod*       RenderMethodDirect3D::s_Instance = NULL;

	RenderMethod& RenderMethodDirect3D::GetSingleton()
	{
		if (!s_Instance) { s_Instance = new RenderMethodDirect3D(); }
		return *s_Instance;
	}

	bool RenderMethodDirect3D::Init()
	{
		//m_WindowHandle = a_WindowHandle;

		m_D3DDevice = NULL;
		m_D3DObject	= NULL;

		m_D3DObject = Direct3DMobileCreate(D3DM_SDK_VERSION);
		if (!m_D3DObject)
		{
			ERROR_EXPLAIN("Failed to make D3D device.");
			return false;
		}

		// set the surface to windowed with a backbuffer

		ZeroMemory(&m_D3DOptions, sizeof(m_D3DOptions));
		m_D3DOptions.Windowed          = TRUE;
		m_D3DOptions.SwapEffect        = D3DMSWAPEFFECT_DISCARD;
		m_D3DOptions.Flags             = D3DMPRESENTFLAG_LOCKABLE_BACKBUFFER;
		m_D3DOptions.BackBufferCount   = 1;
		m_D3DOptions.BackBufferFormat  = D3DMFMT_UNKNOWN;

		// select adapter (video card or default)

		wchar_t* drivers = new wchar_t[256];
		//HRESULT reg_result;

		UINT adapter = D3DMADAPTER_REGISTERED_DEVICE;

		HKEY key;
		bool load_dll = true;

		if (ErrorDirect3D::Failed(RegOpenKeyEx(	
					HKEY_LOCAL_MACHINE,
					TEXT("System\\D3DM\\Drivers"),
					NULL,
					KEY_READ,
					&key
				)
			)
		)
		{
			ERROR_EXPLAIN("Could not open key in registry.");
			load_dll = false;
		}
		else
		{
			DWORD type, length;

			if (RegQueryValueEx(key, TEXT("LocalHook"), NULL, &type, NULL, &length) != ERROR_SUCCESS)
			{
				ERROR_EXPLAIN("Couldn't read value from registry.");
			}
			else
			{
				Log::GetSingleton() << "Length of key value is: " << length << " bytes." << ENDL;

				BYTE data[256];
				if (RegQueryValueEx(key, TEXT("LocalHook"), NULL, &type, (LPBYTE)&data, &length) != ERROR_SUCCESS)
				{
					ERROR_EXPLAIN("Couldn't read value from registry.");
					load_dll = false;
				}
				else
				{
					memcpy(drivers, data, 256 * sizeof(WCHAR));
					Log::GetSingleton() << "Found D3DM driver '" << drivers << "'." << ENDL;
				}
			}
		}

		if (load_dll)
		{
			HMODULE dll = (HMODULE)LoadLibrary(drivers);
			if (dll == NULL)
			{
				Log::GetSingleton() << "Direct3D: Unable to load D3DM reference driver DLL." << ENDL;
				adapter = D3DMADAPTER_DEFAULT;
			}
			else
			{
				void* d3d_init = GetProcAddress(dll, TEXT("D3DM_Initialize"));
				if (d3d_init == NULL)
				{
					Log::GetSingleton() << "Direct3D: Unable to retrieve D3DM reference driver entry point." << ENDL;
					adapter = D3DMADAPTER_DEFAULT;
				}
				else
				{
					HRESULT status = m_D3DObject->RegisterSoftwareDevice(d3d_init);
					if (status != D3DM_OK)
					{
						ErrorDirect3D::Check(status);
						Log::GetSingleton() << "Direct3D: Unable to register D3DM reference driver." << ENDL;
						adapter = D3DMADAPTER_DEFAULT;
					}
				}
			}
		}
		else
		{
			adapter = D3DMADAPTER_DEFAULT;
		}


		if (adapter == D3DMADAPTER_REGISTERED_DEVICE)
		{
			Log::GetSingleton() << "Using manufacturer's Direct3D Mobile driver." << ENDL;
		}
		else if (adapter == D3DMADAPTER_DEFAULT)
		{
			Log::GetSingleton() << "Using emulation driver." << ENDL;
		}

		D3DMADAPTER_IDENTIFIER adapter_id;
		if (ErrorDirect3D::Failed(m_D3DObject->GetAdapterIdentifier(adapter, 0, &adapter_id)))
		{
			ERROR_EXPLAIN("Could not get driver information.");
		}
		else
		{
			Log::GetSingleton() << "Adapter driver: " << adapter_id.Driver << "" << ENDL;
		}

		//D3DMPRESENT_PARAMETERS present;

		HWND window_handle = FindWindow(YG_CLASS, YG_TITLE);

		if (ErrorDirect3D::Failed(m_D3DObject->CreateDevice(	
					adapter, 
					D3DMDEVTYPE_DEFAULT, 
					window_handle, 
					0, 
					&m_D3DOptions, 
					&m_D3DDevice
				)
			)
		)
		{
			ERROR_EXPLAIN("Could not create D3D device.");
			return false;
		}

		if (ErrorDirect3D::Failed(m_D3DDevice->GetDisplayMode(&m_D3DMode)))
		{
			ERROR_EXPLAIN("Failed to get display mode.");
			return false;
		}

		// get pixel format

		Log::GetSingleton() << "Pixel format: ";
		switch (m_D3DMode.Format)
		{
		case D3DMFMT_R8G8B8:
			Log::GetSingleton() << "24-bit RGB (R: 8 bytes, G: 8 bytes, B: 8 bytes)" << ENDL;
			break;
		case D3DMFMT_R5G6B5:
			Log::GetSingleton() << "16-bit RGB (R: 5 bytes, G: 6 bytes, B: 5 bytes)" << ENDL;
			break;
		case D3DMFMT_R3G3B2:
			Log::GetSingleton() << "8-bit RGB (R: 3 bytes, G: 3 bytes, B: 2 bytes)" << ENDL;
			break;
		default:
			Log::GetSingleton() << "Unknown or unsupported (" << m_D3DMode.Format << ")" << ENDL;
			break;
		}

		// get device capabilities

		D3DMCAPS caps;
		if (ErrorDirect3D::Failed(m_D3DDevice->GetDeviceCaps(&caps)))
		{
			ERROR_EXPLAIN("Failed to get device capabilities.");
			return false;
		}

		Log::GetSingleton() << "Max primitives: " << caps.MaxPrimitiveCount << "" << ENDL;
		if (caps.SurfaceCaps & D3DMSURFCAPS_VIDVERTEXBUFFER)
		{
			m_D3DPool = D3DMPOOL_VIDEOMEM;
			Log::GetSingleton() << "Using video memory." << ENDL;
		}
		else
		{
			m_D3DPool = D3DMPOOL_SYSTEMMEM;
			Log::GetSingleton() << "Using system memory." << ENDL;
		}

		int memory = (int)YG_BYTES_TO_MEGABYTES(m_D3DDevice->GetAvailablePoolMem(m_D3DPool));

		// we need more than 2 megabytes

		while (memory < 2)
		{
			if (m_D3DPool == D3DMPOOL_VIDEOMEM)
			{
				ERROR_EXPLAIN("Not enough memory available on the video card.");

				Log::GetSingleton() << "Trying main RAM now." << ENDL;

				m_D3DPool = D3DMPOOL_SYSTEMMEM;
				memory = (int)YG_BYTES_TO_MEGABYTES(m_D3DDevice->GetAvailablePoolMem(m_D3DPool));
			}
			else if (m_D3DPool == D3DMPOOL_SYSTEMMEM)
			{
				ERROR_EXPLAIN("Not enough memory available.", Error::ERROR_FATAL);
			}
		}
		Log::GetSingleton() << "Available memory: " << memory << " MB." << ENDL;

		// go to full screen
		SetFullScreen();

		// turn off 3D options we don't need

		if (ErrorDirect3D::Failed(m_D3DDevice->SetRenderState(D3DMRS_ZENABLE, D3DMZB_FALSE)))
		{
			ERROR_EXPLAIN("Failed to turn off the z buffer.");
			return false;
		}

		if (ErrorDirect3D::Failed(m_D3DDevice->SetRenderState(D3DMRS_CULLMODE, D3DMCULL_NONE)))
		{
			ERROR_EXPLAIN("Failed to turn off culling mode.");
			return false;
		}
		if (ErrorDirect3D::Failed(m_D3DDevice->SetRenderState(D3DMRS_LIGHTING, FALSE)))
		{
			ERROR_EXPLAIN("Failed to turn off lighting.");
			return false;
		}

		// get capabilities

		if (caps.SurfaceCaps & D3DMSURFCAPS_LOCKTEXTURE)
		{
			Log::GetSingleton() << "Device can lock textures." << ENDL;
		}
		else
		{
			Log::GetSingleton() << "Device cannot lock textures." << ENDL;
		}

		if (caps.SurfaceCaps & D3DMSURFCAPS_LOCKRENDERTARGET)
		{
			Log::GetSingleton() << "Device can lock the render target." << ENDL;
		}
		else
		{
			Log::GetSingleton() << "Device cannot lock the render target." << ENDL;
		}

		if (caps.SurfaceCaps & D3DMSURFCAPS_VIDTEXTURE)
		{
			Log::GetSingleton() << "Textures are loaded into video memory." << ENDL;
		}
		else if (caps.SurfaceCaps & D3DMSURFCAPS_SYSTEXTURE)
		{
			Log::GetSingleton() << "Textures are loaded into system memory." << ENDL;
		}

		if (caps.SurfaceCaps & D3DMSURFCAPS_VIDBACKBUFFER)
		{
			Log::GetSingleton() << "Backbuffer is loaded into video memory." << ENDL;
		}
		else if (caps.SurfaceCaps & D3DMSURFCAPS_SYSBACKBUFFER)
		{
			Log::GetSingleton() << "Backbuffer is loaded into system memory." << ENDL;
		}

		Log::GetSingleton() << "Maximum texture size: (" << caps.MaxTextureWidth << ", " << caps.MaxTextureHeight << ")." << ENDL;

		// determine render mode

		if (ErrorDirect3D::Failed( m_D3DDevice->CreateRenderTarget(	
					Device::GetWidth(), 
					Device::GetHeight(), 
					m_D3DMode.Format, 
					D3DMMULTISAMPLE_NONE, 
					TRUE, 
					&m_D3DRenderSurface
				)
			)
		)
		{
			ERROR_EXPLAIN("Failed to construct render target.");

			if (ErrorDirect3D::Failed( m_D3DDevice->CreateTexture(
						Device::GetWidth(),
						Device::GetHeight(),
						1,
						D3DMUSAGE_DYNAMIC | D3DMUSAGE_LOCKABLE,
						D3DMFMT_UNKNOWN,
						m_D3DPool,
						&m_D3DRenderTexture
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to construct render texture.");

				m_RenderMode = OPTION_BACKBUFFER;
			}
			else
			{
				m_RenderMode = OPTION_TOTEXTURE;
			}
		}
		else
		{
			m_RenderMode = OPTION_TOSURFACE;
		}

		// start render mode

		if (m_RenderMode == OPTION_TOSURFACE)
		{
			if (ErrorDirect3D::Failed(m_D3DDevice->GetRenderTarget(&m_D3DOldTarget)))
			{
				ERROR_EXPLAIN("Failed to get old render target.");
				return false;
			}

			D3DMLOCKED_RECT render_rect;
			if (ErrorDirect3D::Failed(m_D3DRenderSurface->LockRect(&render_rect, NULL, NULL)))
			{
				ERROR_EXPLAIN("Failed to lock render surface pixels.");
				return false;
			}
			else
			{
				m_D3DBackSurf = new Surface(Device::GetWidth(), Device::GetHeight(), (Pixel*)render_rect.pBits, render_rect.Pitch);
				m_D3DRenderSurface->UnlockRect();
			}
		}
		else if (m_RenderMode == OPTION_TOTEXTURE)
		{
			D3DMLOCKED_RECT render_rect;
			if (ErrorDirect3D::Failed(m_D3DRenderTexture->LockRect(
						0,             // level
						&render_rect, 
						NULL, 
						NULL
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to lock render texture pixels.");
				return false;
			}
			else
			{
				m_D3DBackSurf = new Surface(Device::GetWidth(), Device::GetHeight(), (Pixel*)render_rect.pBits, render_rect.Pitch);
				m_D3DRenderTexture->UnlockRect(0);
			}
		}
		else if (m_RenderMode == OPTION_BACKBUFFER)
		{
			if (ErrorDirect3D::Failed(m_D3DDevice->GetBackBuffer(
						0, 
						D3DMBACKBUFFER_TYPE_MONO, 
						&m_D3DRenderSurface
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to get backbuffer.");
				return false;
			}

			D3DMLOCKED_RECT render_rect;
			if (ErrorDirect3D::Failed(m_D3DRenderSurface->LockRect(&render_rect, NULL, NULL)))
			{
				ERROR_EXPLAIN("Failed to lock render surface pixels.");
				return false;
			}
			else
			{
				m_D3DBackSurf = new Surface(Device::GetWidth(), Device::GetHeight(), (Pixel*)render_rect.pBits, render_rect.Pitch);
				m_D3DRenderSurface->UnlockRect();
			}
		}

		return true;
	}

	void RenderMethodDirect3D::CleanUp()
	{
		if (m_D3DObject) 
		{ 
			Log::GetSingleton() << "Releasing Direct3D." << ENDL;
			m_D3DObject->Release(); 
		}
		if (m_D3DDevice) 
		{
			Log::GetSingleton() << "Releasing Direct3D device." << ENDL;
			m_D3DDevice->Release(); 
		}
	}

	bool RenderMethodDirect3D::Clear()
	{
		if (m_RenderMode == OPTION_TOSURFACE)
		{
			// save old values

			if (ErrorDirect3D::Failed(m_D3DDevice->GetRenderTarget(
						&m_D3DOldTarget
					)
				)
			)
			{
				ERROR_EXPLAIN("Could not retrieve backbuffer.");
				return false;
			}

			// clear render surface

			if (ErrorDirect3D::Failed(m_D3DDevice->SetRenderTarget(
						m_D3DRenderSurface, 
						NULL
					)
				)
			)
			{
				ERROR_EXPLAIN("Could not set render target to render texture.");
				return false;
			}
		}

		// clear device

		if (ErrorDirect3D::Failed (m_D3DDevice->Clear(	
					0, 
					NULL,                     // target rectangle
					D3DMCLEAR_TARGET, 
					D3DMCOLOR_XRGB(0, 0, 0),  // clear color
					1.0f, 
					0
				)
			)
		)
		{
			ERROR_EXPLAIN("Failed to clear render target.");
			return false;
		}

		return true;
	}

	bool RenderMethodDirect3D::PreRender()
	{
		if (m_RenderMode == OPTION_TOSURFACE)
		{		
			D3DMLOCKED_RECT render_rect;
			if (ErrorDirect3D::Failed(m_D3DRenderSurface->LockRect(
						&render_rect, 
						NULL, 
						NULL
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to lock render surface pixels.");
			}
			else
			{
				m_D3DBackSurf->SetBuffer((Pixel*)render_rect.pBits);
				m_D3DRenderSurface->UnlockRect();
			}
		}
		else if (m_RenderMode == OPTION_TOTEXTURE)
		{
			D3DMLOCKED_RECT render_rect;
			if (ErrorDirect3D::Failed(m_D3DRenderTexture->LockRect(
						0,             // level
						&render_rect, 
						NULL, 
						NULL
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to lock render texture pixels.");
				return false;
			}
			else
			{
				m_D3DBackSurf->SetBuffer((Pixel*)render_rect.pBits);
			}
		}
		else if (m_RenderMode == OPTION_BACKBUFFER)
		{
			if (ErrorDirect3D::Failed(m_D3DDevice->GetBackBuffer(
						0, 
						D3DMBACKBUFFER_TYPE_MONO, 
						&m_D3DRenderSurface
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to get backbuffer.");
				return false;
			}

			D3DMLOCKED_RECT render_rect;
			if (ErrorDirect3D::Failed(m_D3DRenderSurface->LockRect(
						&render_rect, 
						NULL, 
						NULL
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to lock render surface pixels.");
				return false;
			}
			else
			{
				m_D3DBackSurf = new Surface(Device::GetWidth(), Device::GetHeight(), (Pixel*)render_rect.pBits, render_rect.Pitch);
				m_D3DRenderSurface->UnlockRect();
			}
		}

		if (ErrorDirect3D::Failed(m_D3DDevice->BeginScene()))
		{
			ERROR_EXPLAIN("Failed to start rendering.");
			return false;
		}

		return true;
	}

	bool RenderMethodDirect3D::PostRender()
	{
		// ARM architectures do not like globals very much

		LPDIRECT3DMOBILEDEVICE device = m_D3DDevice;

		if (ErrorDirect3D::Failed(device->EndScene()))
		{
			ERROR_EXPLAIN("Failed to end scene.");
			return false;
		}

		if (m_RenderMode == OPTION_TOSURFACE)
		{
			// set render target back

			if (ErrorDirect3D::Failed(device->SetRenderTarget(m_D3DOldTarget, NULL)))
			{
				ERROR_EXPLAIN("Couldn't set render target to backbuffer.");
				return false;
			}

			if (ErrorDirect3D::Failed(device->GetBackBuffer (	
						0, 
						D3DMBACKBUFFER_TYPE_MONO, 
						&m_D3DBack 
					) 
				) 
			)
			{
				ERROR_EXPLAIN("Couldn't retrieve backbuffer.");
				return false;
			}

			RECT dest = { 
				0, 
				0, 
				Device::GetWidth(), 
				Device::GetHeight()
			};

			if (ErrorDirect3D::Failed(device->StretchRect (
						m_D3DRenderSurface, 
						NULL,
						m_D3DBack, 
						&dest, 
						D3DMTEXF_NONE 
					)
				)
			)
			{
				ERROR_EXPLAIN("Failed to stretch render texture to backbuffer.");
				return false;
			}
		}
		else if (m_RenderMode == OPTION_TOTEXTURE)
		{
			m_D3DRenderTexture->UnlockRect(0);
		}

		if (ErrorDirect3D::Failed(device->Present(NULL, NULL, NULL, NULL)))
		{
			ERROR_EXPLAIN("Failed to present device.");
			return false;
		}

		return true;
	}

	void RenderMethodDirect3D::SetFullScreen()
	{
		HWND window_handle = FindWindow(YG_CLASS, YG_TITLE);
		SHFullScreen(window_handle, SHFS_HIDESIPBUTTON | SHFS_HIDETASKBAR);
	}

	void RenderMethodDirect3D::SetClearColor( YGC a_Color )
	{
		m_D3DClear = a_Color;
	}

}; // namespace YoghurtGum